package MySQL::SNMP::InnoDBParser;
require Exporter;

@ISA = qw(Exporter);
@EXPORT_OK = qw(parse_innodb_status);
use strict;
use warnings;

# Math::BigInt reverts to perl only
# automatically if GMP is not installed
use Math::BigInt lib => 'GMP';

sub new {
   bless {}, shift;
}

sub parse_innodb_status {
    my $self = shift;
    my $lines = shift;
    my %status = (
        'current_transactions' => 0,
        'locked_transactions'  => 0,
        'active_transactions'  => 0,
        'current_transactions'  => 0,
        'locked_transactions'   => 0,
        'active_transactions'   => 0,
        'innodb_locked_tables'  => 0,
        'innodb_tables_in_use'  => 0,
        'innodb_lock_structs'   => 0,
        'innodb_lock_wait_secs' => 0,
    );
    my $flushed_to;
    my $innodb_lsn;
    my $purged_to;
    my @spin_waits;
    my @spin_rounds;
    my @os_waits;
    my $txn_seen = 0;

    foreach my $line (@$lines) {
        my @row = split(/ +/, $line);

        # SEMAPHORES
        if ($line =~ m/Mutex spin waits/) {
            push(@spin_waits,  $self->tonum($row[3]));
            push(@spin_rounds, $self->tonum($row[5]));
            push(@os_waits,    $self->tonum($row[8]));
        }
        elsif ($line =~ m/RW-shared spins/) {
            push(@spin_waits, $self->tonum($row[2]));
            push(@spin_waits, $self->tonum($row[8]));
            push(@os_waits,   $self->tonum($row[5]));
            push(@os_waits,   $self->tonum($row[11]));
        }
        # TRANSACTIONS
        elsif ($line =~ m/Trx id counter/) {
            # The beginning of the TRANSACTIONS section: start counting
            # transactions
            # Trx id counter 0 1170664159
            # Trx id counter 861B144C
            $status{'innodb_transactions'} = $self->make_bigint($row[3], $row[4]);
            $txn_seen = 1;
        }
        elsif ($line =~ m/Purge done for trx/) {
            # Purge done for trx's n:o < 0 1170663853 undo n:o < 0 0
            # Purge done for trx's n:o < 861B135D undo n:o < 0
            $purged_to = $self->make_bigint($row[6], $row[7] eq 'undo' ? undef : $row[7]);
            $status{'unpurged_txns'} = $status{'innodb_transactions'} - $purged_to;
        }
        elsif ($line =~ m/History list length/) {
            $status{'history_list'} = $self->tonum($row[3]);
        }
        elsif ($txn_seen && $line =~ m/---TRANSACTION/) {
            $status{'current_transactions'} = $status{'current_transactions'} + 1;
            if ($line =~ m/ACTIVE/) {
                $status{'active_transactions'} = $status{'active_transactions'} + 1;
            }
        }
        elsif ($txn_seen && $line =~ m/------- TRX HAS BEEN/) {
           # ------- TRX HAS BEEN WAITING 32 SEC FOR THIS LOCK TO BE GRANTED:
           $status{'innodb_lock_wait_secs'} = $self->tonum($row[5]);
        }
        elsif ($line =~ m/read views open inside/) {
            $status{'read_views'} = $self->tonum($row[0]);
        }
        elsif ($line =~ m/mysql tables in use/) {
           # mysql tables in use 2, locked 2
           $status{'innodb_tables_in_use'} += $self->tonum($row[4]);
           $status{'innodb_locked_tables'} += $self->tonum($row[6]);
        }
        elsif ($txn_seen && $line =~ m/lock struct\(s\)/) {
            # 23 lock struct(s), heap size 3024, undo log entries 27
            # LOCK WAIT 12 lock struct(s), heap size 3024, undo log entries 5
            # LOCK WAIT 2 lock struct(s), heap size 368
            if ( $line =~ m/LOCK WAIT/ ) {
               $status{'innodb_lock_structs'} += $self->tonum($row[2]);
               $status{'locked_transactions'} += 1;
            }
            else {
                $status{'innodb_lock_structs'} += $self->tonum($row[0]);
            }
        }
        # FILE I/O
        elsif ($line =~ m/OS file reads/) {
            $status{'file_reads'}  = $self->tonum($row[0]);
            $status{'file_writes'} = $self->tonum($row[4]);
            $status{'file_fsyncs'} = $self->tonum($row[8]);
        }
        elsif ($line =~ m/Pending normal aio/) {
            $status{'pending_normal_aio_reads'}  = $self->tonum($row[4]);
            $status{'pending_normal_aio_writes'} = $self->tonum($row[7]);
        }
        elsif ($line =~ m/ibuf aio reads/) {
            $status{'pending_ibuf_aio_reads'} = $self->tonum($row[4]);
            $status{'pending_aio_log_ios'}    = $self->tonum($row[7]);
            $status{'pending_aio_sync_ios'}   = $self->tonum($row[10]);
        }
        elsif ($line =~ m/Pending flushes \(fsync\)/) {
            $status{'pending_log_flushes'}      = $self->tonum($row[4]);
            $status{'pending_buf_pool_flushes'} = $self->tonum($row[7]);
        }
        # INSERT BUFFER AND ADAPTIVE HASH INDEX
        elsif ($line =~ m/^Ibuf for space 0: size /) {
           # Older InnoDB code seemed to be ready for an ibuf per tablespace.  It
           # had two lines in the output.  Newer has just one line, see below.
           # Ibuf for space 0: size 1, free list len 887, seg size 889, is not empty
           # Ibuf for space 0: size 1, free list len 887, seg size 889,
           $status{'ibuf_used_cells'} = $self->tonum($row[5]);
           $status{'ibuf_free_cells'} = $self->tonum($row[9]);
           $status{'ibuf_cell_count'} = $self->tonum($row[12]);
        }
        elsif ($line =~ m/^Ibuf: size /) {
           # Ibuf: size 1, free list len 4634, seg size 4636,
           $status{'ibuf_used_cells'} = $self->tonum($row[2]);
           $status{'ibuf_free_cells'} = $self->tonum($row[6]);
           $status{'ibuf_cell_count'} = $self->tonum($row[9]);
        }
        elsif ($line =~ m/ merged recs, /) {
           # 19817685 inserts, 19817684 merged recs, 3552620 merges
           $status{'ibuf_inserts'} = $self->tonum($row[0]);
           $status{'ibuf_merged'}  = $self->tonum($row[2]);
           $status{'ibuf_merges'}  = $self->tonum($row[5]);
        }
        elsif ($line =~ m/^Hash table size /) {
           # In some versions of InnoDB, the used cells is omitted.
           # Hash table size 4425293, used cells 4229064, ....
           # Hash table size 57374437, node heap has 72964 buffer(s) <-- no used cells
           $status{'hash_index_cells_total'} = $self->tonum($row[3]);
           $status{'hash_index_cells_used'} = $line =~ m/used cells/ ? $self->tonum($row[6]) : '0';
        }
        # LOG
        elsif ($line =~ m/ log i\/o's done, /) {    #'
            $status{'log_writes'} = $self->tonum($row[0]);
        }
        elsif ($line =~ m/ pending log writes, /) {
            $status{'pending_log_writes'}  = $self->tonum($row[0]);
            $status{'pending_chkp_writes'} = $self->tonum($row[4]);
        }
        elsif ($line =~ m/^Log sequence number/) {
            # This number is NOT printed in hex in InnoDB plugin.
            # Log sequence number 13093949495856 //plugin
            # Log sequence number 125 3934414864 //normal
            $innodb_lsn = defined($row[4]) ? $self->make_bigint($row[3], $row[4]) : $self->tonum($row[3]);
        }
        elsif ($line =~ m/^Log flushed up to/) {
            # This number is NOT printed in hex in InnoDB plugin.
            # Log flushed up to   13093948219327
            # Log flushed up to   125 3934414864
            $flushed_to = defined($row[5]) ? $self->make_bigint($row[4], $row[5]) : $self->tonum($row[4]);
        }
        elsif ($line =~ m/^Last checkpoint at/) {
           # Last checkpoint at  125 3934293461
           $status{'last_checkpoint'} = defined($row[4]) ? $self->make_bigint($row[3], $row[4]) : $self->tonum($row[3]);
        }
        # BUFFER POOL AND MEMORY
        elsif ($line =~ m/^Total memory allocated/) {
           # Total memory allocated 29642194944; in additional pool allocated 0
           $status{'total_mem_alloc'}       = $self->tonum($row[3]);
           $status{'additional_pool_alloc'} = $self->tonum($row[8]);
        }
        elsif($line =~ m/Adaptive hash index /) {
           #   Adaptive hash index 1538240664   (186998824 + 1351241840)
           $status{'adaptive_hash_memory'} = $self->tonum($row[4]);
        }
        elsif($line =~ m/Page hash           /) {
           #   Page hash           11688584
           $status{'page_hash_memory'} = $self->tonum($row[3]);
        }
        elsif($line =~ m/Dictionary cache    /) {
           #   Dictionary cache    145525560    (140250984 + 5274576)
           $status{'dictionary_cache_memory'} = $self->tonum($row[3]);
        }
        elsif($line =~ m/File system         /) {
           #   File system         313848   (82672 + 231176)
           $status{'file_system_memory'} = $self->tonum($row[3]);
        }
        elsif($line =~ m/Lock system/) {
           #   Lock system         29232616     (29219368 + 13248)
           $status{'lock_system_memory'} = $self->tonum($row[3]);
        }
        elsif($line =~ m/Recovery system     /) {
           #   Recovery system     0    (0 + 0)
           $status{'recovery_system_memory'} = $self->tonum($row[3]);
        }
        elsif($line =~ m/Threads             /) {
           #   Threads             409336   (406936 + 2400)
           $status{'thread_hash_memory'} = $self->tonum($row[2]);
        }
        elsif($line =~ m/innodb_io_pattern   /) {
           #   innodb_io_pattern   0    (0 + 0)
           $status{'innodb_io_pattern_memory'} = $self->tonum($row[2]);
        }
        elsif ($line =~ m/Buffer pool size /) {
            # The " " after size is necessary to avoid matching the wrong line:
            # Buffer pool size        1769471
            # Buffer pool size, bytes 28991012864
            $status{'pool_size'} = $self->tonum($row[3]);
        }
        elsif ($line =~ m/Free buffers/) {
            $status{'free_pages'} = $self->tonum($row[2]);
        }
        elsif ($line =~ m/Database pages/) {
            $status{'database_pages'} = $self->tonum($row[2]);
        }
        elsif ($line =~ m/Modified db pages/) {
            $status{'modified_pages'} = $self->tonum($row[3]);
        }
        elsif ($line =~ m/Pages read/) {
            $status{'pages_read'}    = $self->tonum($row[2]);
            $status{'pages_created'} = $self->tonum($row[4]);
            $status{'pages_written'} = $self->tonum($row[6]);
        }
        # ROW OPERATIONS
        elsif ($line =~ m/Number of rows inserted/) {
            $status{'rows_inserted'} = $self->tonum($row[4]);
            $status{'rows_updated'}  = $self->tonum($row[6]);
            $status{'rows_deleted'}  = $self->tonum($row[8]);
            $status{'rows_read'}     = $self->tonum($row[10]);
        }
        elsif ($line =~ m/queries inside InnoDB/) {
            $status{'queries_inside'} = $self->tonum($row[0]);
            $status{'queries_queued'} = $self->tonum($row[4]);
        }
    }

    # Derive some values from other values.
    if (defined($innodb_lsn) and defined($flushed_to)) {
	$status{'unflushed_log'} = $innodb_lsn - $flushed_to;
	$status{'log_bytes_written'} = $innodb_lsn;
	$status{'log_bytes_flushed'} = $flushed_to;
	if (defined($status{'last_checkpoint'})) {
	    $status{'uncheckpointed_bytes'} = $status{'log_bytes_written'} - $status{'last_checkpoint'};
	}
    }

    my $val;
    foreach $val (@spin_waits) {
        $status{'spin_waits'} += $val;
    }

    foreach $val (@spin_rounds) {
        $status{'spin_rounds'} += $val;
    }

    foreach $val (@os_waits) {
        $status{'os_waits'} += $val;
    }
    return \%status;
}

# takes only numbers from a string
sub tonum {
    my $self = shift;
    my $str = shift;
    return 0 if !$str;
    return new Math::BigInt $1 if $str =~ m/(\d+)/;
    return 0;
}

# return a 64 bit number from either an hex encoding or
# a hi lo representation
sub make_bigint {
    my ($self, $hi, $lo) = @_;

    unless ($lo) {
        $hi = new Math::BigInt '0x' . $hi;
        return $hi;
    }

    $hi = new Math::BigInt $hi;
    $lo = new Math::BigInt $lo;
    return $lo->badd($hi->blsft(32));
}

# end of package InnoDBParser
1;
